package com.virtual.repository.common.managers;

import java.io.File;
import java.lang.reflect.Constructor;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

import com.virtual.repository.common.model.DataSource;
import com.virtual.repository.common.model.ModelConfiguration;
import com.virtual.repository.common.model.Parameter;
import com.virtual.repository.common.model.adapters.ModelConfigUnmarshallListener;
import com.virtual.repository.common.queries.Query;
import com.virtual.repository.plugins.api.IDatabasePlugin;
import com.virtual.repository.utils.ReflectionUtil;

/**
 * Class responsible for maintaining the whole repository.
 * It maintains the model configuration, opens the connections to the databases and enables querying to them.
 * @author kchaber
 *
 */
public class RepositoryManager extends AbstractRepositoryManager {
	/**
	 * The object holding the configuration model
	 */
	private ModelConfiguration modelConfig;
	
	/**
	 * The map holding all initialized drivers to the appropriate databases.
	 */
	private Map<String, IDatabasePlugin> dbPlugins = new HashMap<>();
	

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void initialize(String configFilePath) {
		if (configFilePath == null) {
			throw new IllegalArgumentException("configFilePath cannot be null");
		}
		
		this.readModel(configFilePath);
		this.initializeDrivers();
		this.openConnections();
		
		this.queryManager.initalize(this.modelConfig, this.dbPlugins);
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void release() {
		this.disconnectDatabases();
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public <TYPE extends Object> Collection<TYPE> executeQuery(Query query) throws Exception {
		return this.queryManager.executeQuery(query);
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public <TYPE extends Object> TYPE executeQueryForSingleObject(Query query) throws Exception {
		return this.queryManager.executeQueryForSingleResult(query);
	}
	
	
	/**
	 * Validates if configuration file is consist with the schema.
	 * @param configFilePath
	 */
	private void validateModelConfig(String configFilePath) {
		throw new UnsupportedOperationException("This method has not been implemented yet");
	}
	
	/**
	 * Reads the model configuration from the appropriate file.
	 * @param configFilePath path to the configuration file.
	 */
	private void readModel(String configFilePath) {
		//this.validateModelConfig(configFilePath);
		
		try {
			File configFile = new File(configFilePath);

			JAXBContext jaxbContext = JAXBContext.newInstance(ModelConfiguration.class);

			Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
			jaxbUnmarshaller.setListener(new ModelConfigUnmarshallListener());
			
			ModelConfiguration modelConfig = (ModelConfiguration) jaxbUnmarshaller.unmarshal(configFile);
			this.modelConfig = modelConfig;
		} catch (Exception ex) {
			throw new IllegalStateException("Could not initialize model due to: " + ex.getCause());
		}
	}
	
	/**
	 * Initializes the drivers by setting the appropriate parameters.
	 * @throws IllegalStateException if at least one driver was not initialized successfully
	 */
	private void initializeDrivers() {
		for (DataSource dataSource : this.modelConfig.getDataSources()) {
			// create a new instance of data source object
			Class<?> databasePluginClass = null;
			try {
				databasePluginClass = Class.forName(dataSource.getImplementationClass());
			} catch (ClassNotFoundException e) {
				throw new IllegalStateException("The data source class: " + dataSource.getImplementationClass() + " not found");
			}
			
			IDatabasePlugin databasePlugin = null;
			try {
				Constructor<?> constructor = databasePluginClass.getConstructor();
				databasePlugin = (IDatabasePlugin) constructor.newInstance();
			} catch (Exception e) {
				throw new IllegalStateException("The no-argument constructor for class: " + dataSource.getImplementationClass() + " not found");
			} 
			

			for (Parameter param : dataSource.getParameters()) {
				ReflectionUtil.setField(databasePlugin, param.getName(), param.getValue());
			}

			//put initialized driver into collection
			this.dbPlugins.put(databasePluginClass.getName(), databasePlugin);
		}
	}
	
	/**
	 * Opens connections to the available databases.
	 * @throws IllegalStateException if at least one database was not connected successfully 
	 */
	private void openConnections() {
		Iterator<Entry<String, IDatabasePlugin>> it = this.dbPlugins.entrySet().iterator();
		while(it.hasNext()) {
			Entry<String, IDatabasePlugin> entry = it.next();
			
			try {
				entry.getValue().connect();
				System.out.println("Connected to the driver: " + entry.getValue().getClass());
			} catch (Exception e) {
				throw new IllegalStateException("Cannot connect to data source: " + entry.getValue().getClass() + " due to: " + e.getMessage());
			}
		}
	}
	
	/**
	 * Disconnects all opened connections to the databases.
	 * @throws IllegalStateException if at least one database was not disconnected successfully 
	 */
	private void disconnectDatabases() {
		Iterator<Entry<String, IDatabasePlugin>> it = this.dbPlugins.entrySet().iterator();
		while(it.hasNext()) {
			Entry<String, IDatabasePlugin> entry = it.next();
			
			try {
				entry.getValue().disconnect();
				System.out.println("Disconnected the driver: " + entry.getValue().getClass());
			} catch (Exception e) {
				throw new IllegalStateException("Exception during disconnection the driver: " + entry.getValue().getClass() + " caused by: " + e.getMessage());
			}
		}
	}
	
	public ModelConfiguration getModelConfig() {
		return modelConfig;
	}

	public void setModelConfig(ModelConfiguration modelConfig) {
		this.modelConfig = modelConfig;
	}
}
